/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.util.evaluators.impl;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.model.objects.XREObject;
import com.hercules.android.receiver.model.objects.XREView;
import com.hercules.android.receiver.util.evaluators.InvalidAndroidReceiverStateException;
import com.hercules.android.receiver.util.evaluators.InvalidDOMPathException;
import com.hercules.android.receiver.util.evaluators.PathEvaluator;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * <p>
 * This class is an implementation of PathEvaluator that evaluates DOM paths according to the section 4.10.1 of the XRE
 * Specification.
 * </p>
 * <p>
 * <em>Sample Code:</em>
 *
 * <pre>
 * // Prepare Android receiver and XRE objects
 * AndroidReceiver receiver = new AndroidReceiver();
 * ApplicationData applicationData = new ApplicationData();
 * receiver.setApplicationData(applicationData);
 * Map&lt;Integer, XREObject&gt; elementsXREIdToObject = new HashMap&lt;Integer, XREObject&gt;();
 * applicationData.setElementsXREIdToObject(elementsXREIdToObject);
 *
 * XREView rootView = new XREView();
 * rootView.setId(2);
 * rootView.setAlpha(0.3);
 *
 * XREView windowView = new XREView();
 * windowView.setId(2001);
 * windowView.setName(&quot;window&quot;);
 * windowView.setAlpha(0.2);
 * windowView.setParent(rootView);
 * rootView.getChildren().add(windowView);
 *
 * XREView titleView = new XREView();
 * titleView.setId(2002);
 * titleView.setName(&quot;title&quot;);
 * titleView.setAlpha(0.1);
 * titleView.setParent(windowView);
 * windowView.getChildren().add(titleView);
 *
 * elementsXREIdToObject.put(2, rootView);
 * elementsXREIdToObject.put(2001, windowView);
 * elementsXREIdToObject.put(2002, titleView);
 *
 * // Create an instance of XREPathEvaluator
 * PathEvaluator pathEvaluator = new XREPathEvaluator(&quot;myLogger&quot;);
 *
 * // Evaluate sample DOM paths
 *
 * int id = pathEvaluator.evaluatePath(&quot;window&quot;, 2002, receiver);
 * // id must be 2001
 *
 * id = pathEvaluator.evaluatePath(&quot;this.children[0].title&quot;, 2, receiver);
 * // id must be 2002
 *
 * id = pathEvaluator.evaluatePath(&quot;this.title&quot;, 2001, receiver);
 * // id must be 2002
 * </pre>
 *
 * </p>
 * <p>
 * <strong>Thread Safety: </strong> This class is immutable and thread safe when AndroidReceiver instance passed to it
 * is used by the caller in thread safe manner.
 * </p>
 *
 * @author saarixx, sparemax
 * @version 1.0
 */
@SuppressWarnings("boxing")
public class XREPathEvaluator implements PathEvaluator {
    /**
     * <p>
     * Represents the class name.
     * </p>
     */
    private static final String CLASS_NAME = XREExpressionEvaluator.class.getName();

    /**
     * <p>
     * Represents the '\.'.
     * </p>
     */
    private static final String ESCAPED_DOT = "\\.";

    /**
     * <p>
     * Represents the path flag 'this'.
     * </p>
     */
    private static final String FLAG_THIS = "this";

    /**
     * <p>
     * Represents the path flag 'this.'.
     * </p>
     */
    private static final String FLAG_PREFIX = "this.";

    /**
     * <p>
     * Represents the path flag 'parent'.
     * </p>
     */
    private static final String FLAG_PARENT = "parent";

    /**
     * <p>
     * Represents the path flag 'children['.
     * </p>
     */
    private static final String FLAG_CHILDREN = "children[";

    /**
     * <p>
     * Represents the path flag '$'.
     * </p>
     */
    private static final String FLAG_ID_0 = "$";

    /**
     * <p>
     * Represents the path flag '#'.
     * <p>
     * AndroidAssembly3 added support for '#' flags
     */
    private static final String FLAG_ID_1 = "#";

    /**
     * <p>
     * The Java logger used by this class for logging errors and debug information.
     * </p>
     * <p>
     * Is null when logging is not required. Is initialized in the constructor and never changed after that. Is used in
     * evaluatePath().
     * </p>
     */
    private final Logger logger;

    /**
     * <p>
     * Creates an instance of XREPathEvaluator.
     * </p>
     *
     * @param loggerName the logger name (<code>null</code> if logging of errors and debug information is not required).
     * @throws IllegalArgumentException if loggerName is empty.
     */
    public XREPathEvaluator(String loggerName) {
        logger = Helper.getLogger(loggerName);
    }

    /**
     * <p>
     * Evaluates the given XRE object DOM path.
     * </p>
     *
     * @param path the DOM path to be evaluated.
     * @param sourceId the ID of the source XRE object.
     * @param receiver the Android receiver.
     * @return the retrieved target XRE object ID.
     * @throws IllegalArgumentException if path is <code>null</code>/empty, sourceId is not positive or receiver is
     * <code>null</code>.
     * @throws InvalidAndroidReceiverStateException if the provided receiver or some XRE object was not properly
     * initialized.
     * @throws InvalidDOMPathException if the provided DOM path is invalid
     */
    public int evaluatePath(String path, int sourceId, AndroidReceiver receiver)
        throws InvalidAndroidReceiverStateException, InvalidDOMPathException {
        // Delegate to evaluatePath(String, int, AndroidReceiver, boolean)
        return evaluatePath(path, sourceId, receiver, false);
    }

    /**
     * <p>
     * Evaluates the given XRE object DOM path. Allows to specify whether custom ID prefixes in format "$id." can be
     * evaluated.
     * </p>
     *
     * @param path the DOM path to be evaluated.
     * @param sourceId the ID of the source XRE object.
     * @param receiver the Android receiver.
     * @param supportCustomId <code>true</code> if custom root XRE object IDs must be supported; <code>false</code>
     * otherwise.
     * @return the retrieved target XRE object ID.
     * @throws IllegalArgumentException if path is <code>null</code>/empty, sourceId is not positive or receiver is
     * <code>null</code>.
     * @throws InvalidAndroidReceiverStateException if the provided receiver or some XRE object was not properly
     * initialized.
     * @throws InvalidDOMPathException if the provided DOM path is invalid
     */
    public int evaluatePath(String path, int sourceId, AndroidReceiver receiver, boolean supportCustomId)
        throws InvalidAndroidReceiverStateException, InvalidDOMPathException {
        Date enterTime = new Date();
        String signature =
            getSignature("evaluatePath(String path, int sourceId, AndroidReceiver receiver,"
                + " boolean supportCustomId)");

        // Log method entry
        /*Helper.logEntrance(logger, signature, new String[]{"path", "sourceId", "receiver", "supportCustomId"},
            new Object[]{path, sourceId, receiver, supportCustomId});*/

        try {
            Helper.checkNullOrEmpty(path, "path");
            Helper.checkPositive(sourceId, "sourceId");
            Helper.checkNull(receiver, "receiver");

            // Get mapping from ID to XRE object
            Map<Integer, XREObject> elementsXREIdToObject = Helper.getElementsXREIdToObject(receiver);

            if (path.startsWith(FLAG_PREFIX + FLAG_PARENT) || path.startsWith(FLAG_PREFIX + FLAG_CHILDREN)) {
                // Remove "this." prefix
                path = path.substring(FLAG_PREFIX.length());
            }
            // Split the given DOM path:
            String[] pathParts = path.split(ESCAPED_DOT);
            for (String pathPart : pathParts) {
                checkPath(pathPart.length() == 0, "The path cannot contain empty parts.");
            }

            // Evaluate the XRE object DOM path
            int curObjectId = evaluatePath(pathParts, elementsXREIdToObject, sourceId, supportCustomId);

            // Log method exit
            //Helper.logExit(logger, signature, new Object[]{curObjectId}, enterTime);

            return curObjectId;
        } catch (IllegalArgumentException e) {
            // Log exception
            throw Helper.logException(logger, signature, e, "IllegalArgumentException is thrown.");
        } catch (InvalidAndroidReceiverStateException e) {
            // Log exception
            throw Helper.logException(logger, signature, e,
                "InvalidAndroidReceiverStateException is thrown when evaluating the XRE object DOM path.");
        } catch (InvalidDOMPathException e) {
            // Log exception
            throw Helper.logException(logger, signature, e,
                "InvalidDOMPathException is thrown when evaluating the XRE object DOM path.");
        }
    }

    /**
     * <p>
     * Evaluates the given XRE object DOM path. Allows to specify whether custom ID prefixes in format "$id." can be
     * evaluated.
     * </p>
     *
     * @param pathParts the path parts.
     * @param elementsXREIdToObject the mapping from ID to XRE object.
     * @param sourceId the source id.
     * @param supportCustomId <code>true</code> if custom root XRE object IDs must be supported; <code>false</code>
     * otherwise.
     * @return the retrieved target XRE object ID.
     * @throws InvalidAndroidReceiverStateException if the provided receiver or some XRE object was not properly
     * initialized.
     * @throws InvalidDOMPathException if the provided DOM path is invalid
     */
    private static int evaluatePath(String[] pathParts, Map<Integer, XREObject> elementsXREIdToObject, int sourceId,
        boolean supportCustomId) throws InvalidAndroidReceiverStateException, InvalidDOMPathException {
        int pathIndex = 1;
        String curPart = pathParts[0];

        XREObject curObject = null;
        if (curPart.equals(FLAG_THIS)) {
            // Get the current view by its ID
            curObject = getView(elementsXREIdToObject, sourceId);
        } else if (curPart.equals(FLAG_PARENT)) {
            // Get the parent view of the current view
            curObject = getView(elementsXREIdToObject, sourceId).getParent();
            Helper.checkState(curObject == null, "The parent view of the current view cannot be null.");
        } else if (curPart.startsWith(FLAG_CHILDREN)) {
            // Get child of the current view
            curObject = getChildView(getView(elementsXREIdToObject, sourceId), curPart);
        } else if (supportCustomId && (curPart.startsWith(FLAG_ID_0) || curPart.startsWith(FLAG_ID_1))) {
            // Parse XRE object ID
            int objectId = parseInt(curPart.substring(1), 1);

            // Get object from the map by its ID
            curObject = elementsXREIdToObject.get(objectId);
            checkPath(curObject == null, "The object with id '" + objectId + "' cannot be null.");
        } else {
            // Get the root view from the map (root XRE view always has ID=2)
            curObject = getView(elementsXREIdToObject, 2);

            pathIndex = 0;
        }

        XREView curView = null;
        if (curObject instanceof XREView) {
            curView = (XREView) curObject;
        }
        if (pathIndex < pathParts.length) {
            checkPath(curView == null, "The current view cannot be null.");

            do {
                curPart = pathParts[pathIndex];

                // Get children of the current vie
                List<XREView> children = curView.getChildren();
                Helper.checkState(children == null, "The children of the current view cannot be null.");

                boolean found = false;
                for (XREView childView : children) {
                    Helper.checkState(childView == null, "The child of the current view cannot be null.");

                    // Get name of the child view
                    if (curPart.equals(childView.getName())) {
                        found = true;
                        curView = childView;

                        // Found
                        break;
                    }
                }
                checkPath(!found, "The child with name '" + curPart + "' cannot be found.");

                curObject = curView;
            } while (++pathIndex < pathParts.length);
        }
        // Get ID of the current XRE object
        return curObject.getId();
    }

    /**
     * <p>
     * Gets the child view of the current view.
     * </p>
     *
     * @param curView the current view.
     * @param curPart the path part.
     * @return the parsed child index.
     * @throws InvalidDOMPathException if the part is invalid or any error occurs.
     * @throws InvalidAndroidReceiverStateException if the current view was not properly initialized.
     */
    private static XREView getChildView(XREView curView, String curPart) throws InvalidDOMPathException,
        InvalidAndroidReceiverStateException {
        // The part should be in the format children[xxx]
        checkPath(!curPart.endsWith("]"), "Missing ']' in the path.");

        int index = parseInt(curPart.substring(FLAG_CHILDREN.length(), curPart.length() - 1), 0);

        // Get children of the current view
        List<XREView> children = curView.getChildren();
        Helper.checkState(children == null, "The children of the current view cannot be null.");
        checkPath(children.size() <= index, "The child view with index '" + index + "' does not exist.");

        // Get child by its index
        XREView childView = children.get(index);
        Helper.checkState(childView == null, "The child view of the current view cannot be null.");

        return childView;
    }

    /**
     * <p>
     * Parses the string as an integer.
     * </p>
     *
     * @param str the string.
     * @param min the minimum value.
     * @return the parsed integer.
     * @throws InvalidDOMPathException if parsed value is less than min or any error occurs.
     */
    private static int parseInt(String str, int min) throws InvalidDOMPathException {
        try {
            int value = Integer.parseInt(str);

            checkPath(value < min, "The value should not be less than '" + min + "'.");

            return value;
        } catch (NumberFormatException e) {
            throw new InvalidDOMPathException("The string '" + str + "' cannot be parsed as an integer.", e);
        }
    }

    /**
     * <p>
     * Gets an object of type XREView.
     * </p>
     *
     * @param elementsXREIdToObject the mapping from ID to XRE object.
     * @param id the id.
     * @return the object of type XREView.
     * @throws InvalidAndroidReceiverStateException if the object is not of type XREView.
     */
    private static XREView getView(Map<Integer, XREObject> elementsXREIdToObject, int id)
        throws InvalidAndroidReceiverStateException {
        XREObject obj = elementsXREIdToObject.get(id);

        Helper.checkState(!(obj instanceof XREView), "The XRE object with id '" + id + "' should be of type XREView.");

        return (XREView) obj;
    }

    /**
     * <p>
     * Checks the path.
     * </p>
     *
     * @param invalidPath the flag indicates whether the path is valid.
     * @param message the error message.
     * @throws InvalidDOMPathException if invalidPath is <code>true</code>.
     */
    private static void checkPath(boolean invalidPath, String message) throws InvalidDOMPathException {
        if (invalidPath) {
            throw new InvalidDOMPathException(message);
        }
    }

    /**
     * <p>
     * Gets the signature for given method for logging.
     * </p>
     *
     * @param method the method name.
     * @return the signature for given method.
     */
    private static String getSignature(String method) {
        return CLASS_NAME + Helper.DOT + method;
    }
}
